﻿#if NETFX_CORE
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
#elif SILVERLIGHT
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
#else
using System.Windows;
using System.Windows.Media;
using System;
using System.ComponentModel;
using System.Windows.Input;
using System.Windows.Threading;
#endif

namespace FreshMeat.Xaml
{
   /// <summary>
   /// An version of Silverlight's ChildWindow that not works the same across platforms.
   /// </summary>
#if SILVERLIGHT
   public class XChildWindow : ChildWindow
#elif NETFX_CORE
   public class XChildWindow : Control // TODO: Figure out how to implement XChildWindow for NETFX_CORE
#else
   public class XChildWindow : Window
#endif
   {
      /// <summary>
      /// Dependency property for <see cref="ChildBackground"/>.
      /// </summary>
      public readonly static DependencyProperty ChildBackgroundProperty = DependencyProperty.Register(
         "ChildBackground",
         typeof(Brush),
         typeof(XChildWindow),
         new FrameworkPropertyMetadata(new SolidColorBrush(new Color { A = 0xff, R = 0xff, G = 0xff, B = 0xff })));

      /// <summary>
      /// Background brush for what the user perceives as the window.
      /// Ideally we'd use the usual Background property but that doesn't work for WPF so this property was created as a substitute.
      /// (In WPF the Background property applies to the actual Window, not the element inside it that looks like the window).
      /// </summary>
      public Brush ChildBackground
      {
         get { return (Brush)GetValue(ChildBackgroundProperty); }
         set { SetValue(ChildBackgroundProperty, value); }
      }

      /// <summary>
      /// Dependency property for <see cref="ChildMargin"/>.
      /// </summary>
      public readonly static DependencyProperty ChildMarginProperty = DependencyProperty.Register(
         "ChildMargin",
         typeof(Thickness),
         typeof(XChildWindow),
         new FrameworkPropertyMetadata(new Thickness()));

      /// <summary>
      /// Margin around what the user perceives as the window.
      /// Ideally we'd use the usual Margin property but that doesn't work for WPF so this property was created as a substitute.
      /// (In WPF the Margin property applies to the actual Window, not the element inside it that looks like the window).
      /// </summary>
      public Thickness ChildMargin
      {
         get { return (Thickness)GetValue(ChildMarginProperty); }
         set { SetValue(ChildMarginProperty, value); }
      }

#if NETFX_CORE
      public bool? DialogResult { get; set; }

      public static readonly DependencyProperty OverlayBrushProperty =
         DependencyProperty.Register("OverlayBrush", typeof(Brush), typeof(XChildWindow), new PropertyMetadata(default(Brush)));

      public Brush OverlayBrush
      {
         get { return (Brush)GetValue(OverlayBrushProperty); }
         set { SetValue(OverlayBrushProperty, value); }
      }

      public static readonly DependencyProperty TitleProperty =
         DependencyProperty.Register("Title", typeof(string), typeof(XChildWindow), new PropertyMetadata(default(string)));

      public string Title
      {
         get { return (string)GetValue(TitleProperty); }
         set { SetValue(TitleProperty, value); }
      }
#endif

#if !SILVERLIGHT && !NETFX_CORE
      const double Extra = 4.0;
      readonly DispatcherTimer timer = new DispatcherTimer();
      FrameworkElement root;
      FrameworkElement chrome;
      FrameworkElement contentRoot;
      bool reallyClose;
      bool? savedDialogResult;
      Dragger dragger;

      public XChildWindow()
      {
         ShowInTaskbar = false;
         WindowStartupLocation = WindowStartupLocation.Manual;
         SizeToContent = SizeToContent.Manual;
         ResizeMode = ResizeMode.NoResize;
         WindowStyle = WindowStyle.None;
         AllowsTransparency = true;
         Background = new SolidColorBrush(Colors.Transparent);
      }

      public override void OnApplyTemplate()
      {
         // Mimic Silverlight by positioning and sizing the child window to completely cover the owner's client area.
         if (Owner != null) 
         {
            Left = Owner.Left + SystemParameters.WindowNonClientFrameThickness.Left + Extra;
            Top = Owner.Top + SystemParameters.WindowNonClientFrameThickness.Top + Extra;

            Width = Owner.ActualWidth - (SystemParameters.WindowNonClientFrameThickness.Left + Extra + SystemParameters.WindowNonClientFrameThickness.Right + Extra);
            Height = Owner.ActualHeight - (SystemParameters.WindowNonClientFrameThickness.Top + Extra + SystemParameters.WindowNonClientFrameThickness.Bottom + Extra);
         }

         reallyClose = false;

         base.OnApplyTemplate();

         root = GetTemplateChild("Root") as FrameworkElement;
         chrome = GetTemplateChild("Chrome") as FrameworkElement;
         contentRoot = GetTemplateChild("ContentRoot") as FrameworkElement;

         if (chrome != null && contentRoot != null && Owner != null)
            dragger = new Dragger(chrome, contentRoot, Owner);

         // Now that we have the root we can update the states
         UpdateVisualStates(true);
      }

      public new void Show()
      {
         throw new InvalidOperationException("Show is not intended to be called; use ShowDialog instead.");
      }

      protected override void OnClosing(CancelEventArgs e)
      {
         if (!reallyClose)
         {
            // The only way to see the closing animation is if we temporarily block
            // the closing of the window, during which time we prevent further interaction,
            // and then 
            savedDialogResult = DialogResult;
            UpdateVisualStates(false);
            var wasEnabled = IsEnabled;
            IsEnabled = false;
            e.Cancel = true;
            timer.Interval = TimeSpan.FromSeconds(0.3);
            timer.Tick += (sender, args) =>
            {
               timer.Stop();
               reallyClose = true;
               IsEnabled = wasEnabled;
               DialogResult = savedDialogResult;
               //Close();
            };
            timer.Start();
         }
         else
         {
            if (dragger != null)
            {
               dragger.Cleanup();
               dragger = null;
            }
         }

         base.OnClosing(e);
      }

      void UpdateVisualStates(bool isOpen, bool useTransitions = true)
      {
         // VisualStateManager has issues when used with a Window so instead we use it with the root.
         if (root != null)
            VisualStateManager.GoToElementState(root, isOpen ? "Open" : "Closed", useTransitions);
      }

      class Dragger
      {
         const double DragTriggerDistance = 2.0;
         const double DragTriggerDistanceSquared = DragTriggerDistance * DragTriggerDistance;

         readonly FrameworkElement element;
         readonly FrameworkElement target;
         TranslateTransform translate;
         readonly Window parent;
         bool isPossibleStart;
         Point mouseDownPoint;
         bool isDragging;
         Point cummulativeDrags;
         bool isHandlingEvent;

         public Dragger(FrameworkElement element, FrameworkElement target, Window parent)
         {
            this.element = element;
            this.target = target;
            this.parent = parent;
            
            cummulativeDrags = new Point(0, 0);

#if NETFX_CORE
            element.PointerPressed += MouseLeftButtonDown;
            element.PointerReleased += MouseLeftButtonUp;
            element.PointerMoved += MouseMove;
#else
            element.MouseLeftButtonDown += MouseLeftButtonDown;
            element.MouseLeftButtonUp += MouseLeftButtonUp;
            element.MouseMove += MouseMove;
#endif
         }

         public void Cleanup()
         {
#if NETFX_CORE
            element.PointerPressed -= MouseLeftButtonDown;
            element.PointerReleased -= MouseLeftButtonUp;
            element.PointerMoved -= MouseMove;
#else
            element.MouseLeftButtonDown -= MouseLeftButtonDown;
            element.MouseLeftButtonUp -= MouseLeftButtonUp;
            element.MouseMove -= MouseMove;
#endif
         }

         /// <summary>
         /// Handles the left mouse button going down, which is the first step towards starting a drag.
         /// </summary>
#if NETFX_CORE
         void MouseLeftButtonDown(object sender, PointerRoutedEventArgs e)
#else
         void MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
#endif
         {
            if (e.Handled || isPossibleStart || isDragging)
               return;

            e.Handled = true;

            mouseDownPoint = GetMousePosition(e);

            isPossibleStart = true;
         }

         /// <summary>
         /// Handles the mouse move, which is either the second step towards starting a drag or
         /// is a continuation of a drag operation.
         /// </summary>
#if NETFX_CORE
         void MouseMove(object sender, PointerRoutedEventArgs e)
#else
         void MouseMove(object sender, MouseEventArgs e)
#endif
         {
            // Prevent reentrency -- see mouse capture below
            if (isHandlingEvent)
               return;
            isHandlingEvent = true;

            if (isPossibleStart)
            {
               // Mouse must move at least the trigger distance from the original point
               // where it went down in order to start a drag operation.
               var point = GetMousePosition(e);
               var a = point.X - mouseDownPoint.X;
               var b = point.Y - mouseDownPoint.Y;
               if (a * a + b * b > DragTriggerDistanceSquared)
               {
                  isPossibleStart = false;

                  // If mouse can be captured then start dragging.
                  if (element.CaptureMouse()) // Calling this results in MouseMove firing again, hence the need to block reentrency
                     isDragging = true;
               }
            }
            // Don't make this an "else" clause -- if the drag was started above then this must be done immediately.
            // This also handles drags that were already in progress.
            if (isDragging)
               MoveTo(e);

            isHandlingEvent = false;
         }

         /// <summary>
         /// Handles the left mouse button being released, which either aborts a drag
         /// before it ever gets going or ends an actual drag operation.
         /// </summary>
#if NETFX_CORE
         void MouseLeftButtonUp(object sender, PointerRoutedEventArgs e)
#else
         void MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
#endif
         {
            if (e.Handled || (!isPossibleStart && !isDragging))
               return;

            e.Handled = true;

            if (isDragging)
            {
               var offset = MoveTo(e);
               cummulativeDrags.X += offset.X;
               cummulativeDrags.Y += offset.Y;
            }

            isPossibleStart = false;
            isDragging = false;

            // Calling this results in MouseMove firing again so this must be done after
            // we've cleared all the flags so that MouseMove won't do anything.
            element.ReleaseMouseCapture(); 
         }

#if NETFX_CORE
         Point GetMousePosition(PointerRoutedEventArgs e)
#else
         Point GetMousePosition(MouseEventArgs e)
#endif
         {
            return element.PointToScreen(e.GetPosition(element));
         }

#if NETFX_CORE
         Point MoveTo(PointerRoutedEventArgs e)
#else
         Point MoveTo(MouseEventArgs e)
#endif
         {
            var position = GetMousePosition(e);

            var minX = parent.Left + SystemParameters.WindowNonClientFrameThickness.Left + Extra;
            if (position.X < minX)
               position.X = minX;

            var minY = parent.Top + SystemParameters.WindowNonClientFrameThickness.Top + Extra;
            if (position.Y < minY)
               position.Y = minY;
            
            var maxX = parent.Left + parent.ActualWidth - (SystemParameters.WindowNonClientFrameThickness.Right + Extra);
            if (position.X > maxX)
               position.X = maxX;

            var maxY = parent.Top + parent.ActualHeight - (SystemParameters.WindowNonClientFrameThickness.Bottom + Extra);
            if (position.Y > maxY)
               position.Y = maxY;

            var offset = new Point(position.X - mouseDownPoint.X, position.Y - mouseDownPoint.Y);

            // We waited until here to prepare the translate transform because by now, animations will have
            // occurred which (somehow?) unfreeze the existing transforms, allowing us to modify them.  Doing 
            // this earlier (for example, at construction time) results in exceptions because everything is frozen.
            PrepareTranslate();

            translate.X = cummulativeDrags.X + offset.X;
            translate.Y = cummulativeDrags.Y + offset.Y;

            return offset;
         }

         void PrepareTranslate()
         {
            if (translate != null) 
               return;

            var transformGroup = target.RenderTransform as TransformGroup;
            if (transformGroup == null)
            {
               transformGroup = new TransformGroup();
               transformGroup.Children.Add(target.RenderTransform);
               target.RenderTransform = transformGroup;
            }

            translate = new TranslateTransform();
            transformGroup.Children.Add(translate);
         }
      }

      public readonly static DependencyProperty OverlayBrushProperty = DependencyProperty.Register(
         "OverlayBrush",
         typeof(Brush),
         typeof(XChildWindow),
         new FrameworkPropertyMetadata(new SolidColorBrush(new Color { A = 0x7f, R = 0x00, G = 0x00, B = 0x00 })));

      public Brush OverlayBrush
      {
         get { return (Brush)GetValue(OverlayBrushProperty); }
         set { SetValue(OverlayBrushProperty, value); }
      }

      public readonly static DependencyProperty OverlayOpacityProperty = DependencyProperty.Register(
         "OverlayOpacity",
         typeof(double),
         typeof(XChildWindow),
         new FrameworkPropertyMetadata(1.0));

      public double OverlayOpacity
      {
         get { return (double)GetValue(OverlayOpacityProperty); }
         set { SetValue(OverlayOpacityProperty, value); }
      }
#endif
   }
}
